package topology;

import java.io.BufferedWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import util.APILoader;
import util.XMLUtils;
import util.ConfigInfo.ElementTag;
import util.ConfigInfo.ElementType;
import bgpemulation.AS;

public class GraphNode {

	public static int nextPort = 9000;
	public static Random rng = new Random(nextPort);
	
	public static Map<Integer, GraphNode> connectedNodes = new HashMap<Integer, GraphNode>();
	public static ArrayList<GraphNode> connectedNodesOrder = new ArrayList<GraphNode>();
	
	private int port;
	private Map<Integer, GraphNode> peers = new TreeMap<Integer, GraphNode>();
	private Map<Integer, GraphNode> protocolPeers = new TreeMap<Integer, GraphNode>();
	private String apiClassName = bgpemulation.AS.class.getName();
	
	public GraphNode(){
		port = nextPort++;
	}
	
	public GraphNode(boolean connected){
		this();
		if(connected){
			connect();
		}
	}
	
	public void protocolPeer(GraphNode pp){
		protocolPeers.put(pp.port, pp);
	}
	
	public void peer(GraphNode dcNode) {
		peers.put(dcNode.port, dcNode);
		if(dcNode.isConnected() || isConnected()){
			connect();
		}
	}

	public boolean peersWith(GraphNode potentialNeighbor) {
		if(this.equals(potentialNeighbor)){
			return true;
		}
		return false;
	}

	@Override
	public boolean equals(Object o){
		if(!o.getClass().equals(this.getClass())){
			return false;
		}
		GraphNode other = (GraphNode)o;
		if(port==other.port){
			return true;
		}
		if(peers.containsKey(other.port)){
			return true;
		}
		if(other.peers.containsKey(port)){
			return true;
		}
		return false;
	}

	public void serialize(Document base) {
		Element node = base.createElement(ElementTag.NODE.tag());
		Element nodeConfig = base.createElement(ElementTag.NODE_CONFIG.tag());
		Element events = base.createElement(ElementTag.EVENTS.tag());
		Element topology = base.createElement(ElementTag.TOPOLOGY.tag());
		appendChildren(node, new Element[]{nodeConfig, events, topology});
		setClass(nodeConfig, base);
		addPorts(nodeConfig, base);
		addNeighbors(topology, base, AS.PEER, peers);
		addNeighbors(topology, base, AS.PROTOCOL, protocolPeers);
		base.getFirstChild().appendChild(node);
	}

	private void setClass(Element nodeConfig, Document base) {
		Element nodeClass = base.createElement(ElementTag.API_CLASS_NAME.tag());
		nodeClass.setTextContent(getAPIClass());
		appendChildren(nodeConfig, new Element[]{nodeClass});
	}

	private String getAPIClass() {
		return apiClassName;
	}
	
	public void setAPIClass(String name){
		apiClassName = name;
	}
	
	public boolean isTrusted(){
		return getAPIClass().matches("bgpemulation.TrustedAS");
	}

	private void addNeighbors(Element topology, Document base, String direction, Map<Integer, GraphNode> peerMap) {
		for(Integer peer : peerMap.keySet()){
			addNeighbor(base, topology, peer, direction);
		}
	}

	private void addNeighbor(Document base, Element topology, Integer peer, String dir) {
		Element neighbor = base.createElement(ElementTag.NEIGHBOR.tag());
		Element address = base.createElement(ElementTag.ADDRESS.tag());
		Element protocol = base.createElement(ElementTag.PROTOCOL.tag());
		Element direction = base.createElement(ElementTag.DIRECTION.tag());
		address.setTextContent(APILoader.LOCAL_IP_KEYWORD + ":" + peer);
		protocol.setTextContent(ElementType.TCP.typeName());
		direction.setTextContent(dir);
		appendChildren(neighbor, new Element[]{address,protocol,direction});
		topology.appendChild(neighbor);
	}

	private void addPorts(Element nodeConfig, Document base) {
		Element tcpPort = createPortElement(base, ElementType.TCP.typeName());
		Element udpPort = createPortElement(base, ElementType.UDP.typeName());
		appendChildren(nodeConfig, new Element[]{tcpPort, udpPort});
	}

	private Element createPortElement(Document base, String name) {
		Element port = base.createElement(ElementTag.PORT.tag());
		port.setAttribute(XMLUtils.TYPE, name);
		port.setTextContent("" + this.port);
		return port;
	}

	private void appendChildren(Element base, Element[] children) {
		for(Element child : children){
			base.appendChild(child);
		}
	}
	
	public boolean isConnected(){
		return connectedNodes.containsKey(port);
	}
	
	private void connect(){
		if(!isConnected()){
			connectedNodes.put(port, this);
			connectedNodesOrder.add(this);
			for(Integer port : peers.keySet()){
				peers.get(port).connect();
			}
		}
	}
	
	public void randomConnect(){
		peer(getConnectedNode());
	}
	
	public Integer getPort(){
		return port;
	}

	private GraphNode getConnectedNode(){
		return connectedNodesOrder.get(rng.nextInt(connectedNodesOrder.size()));
	}

	public void printPeers(){
		if(!peers.isEmpty()){
			for(Integer peerPort : peers.keySet()){
				System.out.println(port + " " + peerPort);
			}
		}
	}

	public void writeNode(BufferedWriter bw) throws IOException {
		bw.write("\t" + nodeName() + "[label=\"" + port + "\"];\n");
	}

	public void writePeers(BufferedWriter bw) throws IOException {
		for(GraphNode node : peers.values()){
			bw.write("\t" + nodeName() + "--" + node.nodeName() + ";\n");
		}
	}
	
	private String nodeName(){
		return "node" + port;
	}
}
